package com.hmdp.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.LogicExpireDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @program: hm-dianping
 * @description: redis缓存工具类
 * @author: Feng
 * @create: 2023-03-09 06:24
 **/

//要用这个类的话要注入
@Component
public class RedisClient {
    @Autowired
//    只能注入非静态的，静态的要用set
    private StringRedisTemplate stringRedisTemplate;

    //    使用的是ExecutorService
    private static ExecutorService POOL_EXECUTOR = Executors.newSingleThreadExecutor();

    public void set(String key, Object value, Long ttl, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), ttl, unit);
    }

    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * @Description: 缓存穿透
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/08 16:02
     */
    public <ID, R> R cachePenetration(ID id, String prefix, Class<R> r, Function<ID, R> query) {
        //从缓存中查询
        String cacheKey = prefix + id;
        String dataJson = get(prefix + id);
//        //用函数来判断，
        if (StrUtil.isNotBlank(dataJson)) {
            //使用json对字符串反序列化
            R result = JSONUtil.toBean(dataJson, r);
            return result;
        }
//        //对""进行判断,直接返回null
        if (dataJson != null) return null;

        //不存在，则查询数据库放入缓存
        R result = query.apply(id);

        if (result == null) {
            set(cacheKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        set(cacheKey, result, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return result;
    }


    /**
     * @Description: 互斥锁，逻辑过期
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/09 6:16
     */
    public <ID, R> R cacheBreakdownWithLogic(ID id, String cacheKeyPrefix, Class<R> r, Function<ID, R> query, String lockKeyPrefix, Long ttl, TimeUnit timeUnit) {
        //从缓存中查询
        String cacheKey = cacheKeyPrefix + id;
        String dtoJson = get(cacheKey);
        //用函数来判断，
//        //如果为空的话就直接返回null，表示不是热点数据
        if (StrUtil.isBlank(dtoJson)) {
            return null;
        }

        //使用json对字符串反序列化
        LogicExpireDTO dto = JSONUtil.toBean(dtoJson, LogicExpireDTO.class);
        LocalDateTime expire = dto.getExpire();
//        //json转成bean，取这个bean的object类型的属性需要转成JSONObject
//        //由于不知道data的类型会转成JSONObject
        JSONObject data = (JSONObject) dto.getData();
        R result = JSONUtil.toBean(data, r);

        if (expire.isAfter(LocalDateTime.now())) return result;


        String lock = lockKeyPrefix + id;

        //获取不到锁，直接返回旧数据
        if (!tryLock(lock)) {
            return result;
        }
        //双重锁检验
        dtoJson = get(cacheKey);
        if (StrUtil.isBlank(dtoJson)) {
            return null;
        }
        dto = JSONUtil.toBean(dtoJson, LogicExpireDTO.class);
        expire = dto.getExpire();
        data = (JSONObject) dto.getData();
        result = JSONUtil.toBean(data, r);
        if (expire.isAfter(LocalDateTime.now())) return result;


        //开启线程进行缓存重建，返回旧数据
        POOL_EXECUTOR.submit(() -> {
            try {
                Thread.sleep(200);
                //直接建预热数据就行了，其中有查询数据库的语句
//                这里分为两步，查询数据和放入缓存
                R newData = query.apply(id);
                setWithLogic(cacheKey, newData, ttl, timeUnit);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //重建完再释放锁
                //注意数据库查询完了才能释放缓存
                releaseLock(lock);
            }

        });


        return result;
    }

    private void setWithLogic(String cacheKey, Object newData, Long ttl, TimeUnit timeUnit) {
//        直接timeUnit.toSecond
        LogicExpireDTO dto = new LogicExpireDTO(newData, LocalDateTime.now().plusSeconds(timeUnit.toSeconds(ttl)));
        stringRedisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(dto));
    }


    private void releaseLock(String lock) {
        stringRedisTemplate.delete(lock);
    }

    private boolean tryLock(String lock) {
        return stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
    }
}
